package com.ibeeking.found.common.db.util;

import cn.hutool.db.DbUtil;
import cn.hutool.db.dialect.impl.MysqlDialect;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.HikariDataSourceCreator;
import com.baomidou.dynamic.datasource.ds.GroupDataSource;
import com.baomidou.dynamic.datasource.ds.ItemDataSource;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.hikari.HikariCpConfig;
import com.baomidou.dynamic.datasource.toolkit.CryptoUtils;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import com.ibeeking.found.common.config.feign.GlobalClient;
import com.ibeeking.found.common.constants.HeaderNounConstant;
import com.ibeeking.found.common.db.annotation.Read;
import com.ibeeking.found.common.db.annotation.Write;
import com.ibeeking.found.common.db.config.prop.MasterSlaveDataSourceProp;
import com.ibeeking.found.common.db.enums.WriteOrRead;
import com.ibeeking.found.common.entity.DatasourceConfigDTO;
import com.ibeeking.found.common.response.ResponseCodeEnum;
import com.ibeeking.nematos.core.utils.SpringBeanUtil;
import com.ibeeking.nematos.exception.BusinessException;
import com.ibeeking.nematos.redis.utils.RedisUtils;
import com.ibeeking.nematos.utils.data.Collections3;
import com.ibeeking.nematos.utils.data.ObjectUtils;
import com.ibeeking.nematos.utils.data.StringUtils;
import com.ibeeking.nematos.utils.result.ResponseResult;
import com.mchange.v1.db.sql.ConnectionUtils;
import com.mysql.cj.MysqlConnection;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.logging.log4j.ThreadContext;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @ClassName DataSourceUtils
 * @Description
 * @Author ibeeking
 * @Date 2021-03-10 8:47
 **/
@Component
public class DataSourceUtils extends org.springframework.jdbc.datasource.DataSourceUtils {
    private static final Logger log = LoggerFactory.getLogger(DataSourceUtils.class);

    private static final String DB = "DB:";
    private static final String DB_SOURCE = "DB_SOURCE:";
    private static final Integer MAX_COUNT = 5;

    private static DataSource dataSource;
    private static MasterSlaveDataSourceProp masterSlaveDataSourceProp;
    private static HikariDataSourceCreator hikariDataSourceCreator;
    private static RedisUtils redisUtils;

    public DataSourceUtils(DataSource dataSource, MasterSlaveDataSourceProp masterSlaveDataSourceProp, HikariDataSourceCreator hikariDataSourceCreator, RedisUtils redisUtils) {
        DataSourceUtils.dataSource = dataSource;
        DataSourceUtils.masterSlaveDataSourceProp = masterSlaveDataSourceProp;
        DataSourceUtils.hikariDataSourceCreator = hikariDataSourceCreator;
        DataSourceUtils.redisUtils = redisUtils;
    }

    public static Connection getConnection() {
        Connection conn = null;
        try {
            conn = doGetConnection(dataSource);
            if (null == conn) {
                throw new BusinessException("获取数据源连接失败");
            }
        } catch (SQLException e) {
            throw new BusinessException("获取数据源连接失败");
        }
        return conn;
    }

    public static Connection getConnection(String host, Integer port, String dbName, String userName, String password) {
        Connection conn;
        try {
            //加载驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
            conn = DriverManager.getConnection("jdbc:mysql://" + host + ":" + port + "/" + dbName, userName, password);
            if (null == conn) {
                throw new BusinessException("获取数据源连接失败");
            } else {
                return conn;
            }
        } catch (Exception ex) {
            throw new BusinessException("获取数据源连接失败");
        }
    }

    /**
     * 加载主从数据源
     *
     * @param writeOrRead
     * @return
     */
    public static boolean loadReadWriteDateSource(WriteOrRead writeOrRead) {
        boolean write = write(writeOrRead);
        //获取当前租户数据源组名
        String primary = masterSlaveDataSourceProp.getPrimary();
        String primaryPrefix = primary.substring(0, primary.indexOf("_"));
        //要切换的数据源组
        String chooseGroup = "";
        //初始数据源配置
        Map<String, Map<String, String>> initDatasource = masterSlaveDataSourceProp.getDatasource();
        for (String key : initDatasource.keySet()) {
            if (write) {
                chooseGroup = primaryPrefix;
                break;
            } else {
                if (!key.contains(primaryPrefix)) {
                    chooseGroup = key.substring(0, key.indexOf("_"));
                }
            }
        }
        //切换数据源
        DynamicDataSourceContextHolder.push(chooseGroup);
        return true;
    }

    /**
     * 加载数据源
     *
     * @return
     */
    public static boolean loadDataSource() {
        return loadDataSource(WriteOrRead.WRITE);
    }

    /**
     * 加载数据源
     *
     * @param writeOrRead
     * @return
     */
    public static boolean loadDataSource(WriteOrRead writeOrRead) {
        String tenantId = ThreadContext.get(HeaderNounConstant.TENANT_ID);
        if (StringUtils.isBlank(tenantId)) {
            throw new BusinessException("请选择租户切换数据源");
        }
        return loadDataSource(tenantId, writeOrRead);
    }

    /**
     * 加载数据源
     *
     * @param tenantId    租户
     * @param writeOrRead 读/写库
     */
    public static boolean loadDataSource(String tenantId, WriteOrRead writeOrRead) {
        boolean write = write(writeOrRead);
        //获取当前租户数据源组名
        String primary = masterSlaveDataSourceProp.getPrimary();
        String primaryPrefix = getKeyPrefix(primary);
        //初始数据源配置
        Map<String, Map<String, String>> initDatasource = masterSlaveDataSourceProp.getDatasource();
        //本地线程数据源
        DynamicRoutingDataSource dynamicDataSource = (DynamicRoutingDataSource) dataSource;
        Map<String, GroupDataSource> currentGroupDataSources = dynamicDataSource.getCurrentGroupDataSources();
        Map<String, DataSource> currentDataSources = dynamicDataSource.getCurrentDataSources();
        //初始化数据源的所有key
        Set<String> initDbKeys = initDatasource.keySet();

        //要切换的数据源组
        String chooseGroup = "";
        if (write) {
            chooseGroup = primaryPrefix + tenantId;
        } else {
            for (String key : initDbKeys) {
                //获取数据源组
                if (!key.contains(primaryPrefix)) {
                    chooseGroup = getKeyPrefix(key) + tenantId;
                    break;
                }
            }
        }
        if (StringUtils.isBlank(chooseGroup)) {
            throw new BusinessException(ResponseCodeEnum.DB_CHANGE_ERROR.getCode(), ResponseCodeEnum.DB_CHANGE_ERROR.getMsg());
        }
        if (!currentGroupDataSources.containsKey(chooseGroup)) {
            //加载数据源
            List<String> poolKeys = new ArrayList<>();
            if (write) {
                poolKeys.add(chooseGroup + getKeySuffix(primary));
            } else {
                for (String key : initDbKeys) {
                    //获取需要初始化的数据源
                    String keyPrefix = getKeyPrefix(key);
                    String initPoolKeyPrefix = keyPrefix + getInitTenant(key);
                    String poolKey = keyPrefix + tenantId + getKeySuffix(key);
                    if (!key.contains(primaryPrefix) && key.contains(initPoolKeyPrefix)) {
                        poolKeys.add(poolKey);
                    }
                }
            }
            if (Collections3.isEmpty(poolKeys)) {
                throw new BusinessException(ResponseCodeEnum.DB_CHANGE_ERROR.getCode(), ResponseCodeEnum.DB_CHANGE_ERROR.getMsg());
            }
            if (redisUtils.tryLock(DB + tenantId)) {
                try {
                    for (String poolKey : poolKeys) {
                        if (!currentDataSources.containsKey(poolKey)) {
                            loadDataSource(poolKey, tenantId, primary, primaryPrefix, dynamicDataSource, currentDataSources);
                        }
                    }
                } catch (Exception e) {
                    log.error("[{}][{}]，请重试", chooseGroup, ResponseCodeEnum.DB_CHANGE_ERROR.getMsg(), e);
                    return false;
                } finally {
                    redisUtils.unlock(DB + tenantId);
                }
            }
        }
        //切换数据源
        DynamicDataSourceContextHolder.push(chooseGroup);
        return true;
    }

    /**
     * 加载新数据源
     *
     * @param poolKey            数据源名
     * @param tenantId           新数据源租户id
     * @param primary            初始主数据源名
     * @param primaryPrefix      主数据源前缀
     * @param dynamicDataSource                 多数据源路由
     * @param currentDataSources 所有数据源集合
     */
    private static void loadDataSource(String poolKey, String tenantId, String primary, String primaryPrefix, DynamicRoutingDataSource dynamicDataSource, Map<String, DataSource> currentDataSources) {
        String initName = getKeyPrefix(poolKey) + getInitTenant(primary) + getKeySuffix(poolKey);
        Map<String, String> propMap = masterSlaveDataSourceProp.getDatasource().get(initName);
        //获取初始数据源
        DataSource dataSource = currentDataSources.get(initName);
        ItemDataSource itemDataSource = (ItemDataSource) dataSource;
        HikariDataSource hikariDataSource = (HikariDataSource) itemDataSource.getRealDataSource();
        //配置新数据源参数
        DataSourceProperty dsProperty = new DataSourceProperty();
        dsProperty.setPoolName(poolKey);
        dsProperty.setType(HikariDataSource.class);
        dsProperty.setDriverClassName(hikariDataSource.getDriverClassName());
        String jdbcUrl = hikariDataSource.getJdbcUrl();
        String userName = hikariDataSource.getUsername();
        String password = hikariDataSource.getPassword();
        log.info(">>>>>>>>>>:user  "+userName+", pass:[" +password+ "]");
        DatasourceConfigDTO tenantDatasource = DataSourceUtils.getTenantDatasource(tenantId);
        // 替换为租户发布的数据源
        if(tenantDatasource!=null) {
            try {
                Integer port = tenantDatasource.getPort();
                String host = CryptoUtils.decrypt(tenantDatasource.getHost());
                userName = CryptoUtils.decrypt(tenantDatasource.getUserName());
                password = CryptoUtils.decrypt(tenantDatasource.getPassword());
                jdbcUrl = jdbcUrl.replaceFirst("//[^/]+/", "//" + host + ":" + port + "/");
            }catch(Exception ex){
                jdbcUrl = hikariDataSource.getJdbcUrl();
                userName = hikariDataSource.getUsername();
                password = hikariDataSource.getPassword();
            }
        }
        String dbName = jdbcUrl.substring(jdbcUrl.lastIndexOf("/") + 1, jdbcUrl.indexOf("?"));
        jdbcUrl = jdbcUrl.split(dbName)[0] + dbName.substring(0, dbName.lastIndexOf("_") + 1) + tenantId + jdbcUrl.split(dbName)[1];
        log.info(">>>>>>>>>>:Switch to Datasource for "+initName+", JdbcUrl:[" +jdbcUrl+ "]");
        dsProperty.setUrl(jdbcUrl);
        dsProperty.setUsername(userName);
        dsProperty.setPassword(password);
        dsProperty.setSchema(hikariDataSource.getSchema());
        dsProperty.setSeata(itemDataSource.getSeata());
        dsProperty.setP6spy(itemDataSource.getP6spy());
        dsProperty.setSeparator(propMap.get("separator"));
        //配置数据库连接池参数
        HikariCpConfig hikariCpConfig = new HikariCpConfig();
        hikariCpConfig.setPoolName(poolKey);
        hikariCpConfig.setConnectionTimeout(hikariDataSource.getConnectionTimeout());
        hikariCpConfig.setIdleTimeout(hikariDataSource.getIdleTimeout());
        hikariCpConfig.setMaxLifetime(hikariDataSource.getMaxLifetime());
        hikariCpConfig.setMinIdle(hikariDataSource.getMinimumIdle());
        hikariCpConfig.setMaxPoolSize(hikariDataSource.getMaximumPoolSize());
        hikariCpConfig.setCatalog(hikariDataSource.getCatalog());
        hikariCpConfig.setValidationTimeout(hikariDataSource.getValidationTimeout());
        hikariCpConfig.setLeakDetectionThreshold(hikariDataSource.getLeakDetectionThreshold());
        hikariCpConfig.setInitializationFailTimeout(hikariDataSource.getInitializationFailTimeout());
        hikariCpConfig.setConnectionInitSql(hikariDataSource.getConnectionInitSql());
        hikariCpConfig.setConnectionTestQuery(hikariDataSource.getConnectionTestQuery());
        if (poolKey.contains(primaryPrefix)) {
            hikariCpConfig.setIsReadOnly(false);
        } else {
            hikariCpConfig.setIsReadOnly(true);
        }
        dsProperty.setHikari(hikariCpConfig);
        DataSource newDataSource = hikariDataSourceCreator.createDataSource(dsProperty);
        DataSource itemDs = new ItemDataSource(poolKey, newDataSource, newDataSource, itemDataSource.getP6spy(), itemDataSource.getSeata(), itemDataSource.getSeataMode());
        dynamicDataSource.addDataSource(poolKey, itemDs);
    }

    /**
     * 清空数据源
     */
    public static void clear() {
        DynamicDataSourceContextHolder.clear();
    }


    /**
     * 获取租户数据源
     *
     * @param tenantId
     * @return
     */
    private static DatasourceConfigDTO getTenantDatasource(String tenantId) {
        try {
            Object obj = redisUtils.get(DB_SOURCE + tenantId);
            if(ObjectUtils.isNotNull(obj)){
                DatasourceConfigDTO datasourceConfigDTO = (DatasourceConfigDTO)obj;
                if(datasourceConfigDTO!=null) return datasourceConfigDTO;
                else return null;
            }else {
                GlobalClient globalFeignClient = SpringBeanUtil.getBean(GlobalClient.class);
                ResponseResult<DatasourceConfigDTO> responseResult = globalFeignClient.queryTenantDatasource(Integer.valueOf(tenantId));
                if (responseResult == null || !ResponseCodeEnum.SUCCESS.getCode().equals(responseResult.getCode()))
                    return null;
                redisUtils.set(DB_SOURCE + tenantId, responseResult.getData());
                return responseResult.getData();
            }
        }catch (Exception ex){
            return null;
        }
    }

    /**
     * 获取key前缀
     *
     * @param key
     * @return
     */
    private static String getKeyPrefix(String key) {
        return key.substring(0, key.indexOf("-") + 1);
    }

    /**
     * 获取key后缀
     *
     * @param key
     * @return
     */
    private static String getKeySuffix(String key) {
        return key.substring(key.indexOf("_"));
    }

    /**
     * 获取初始租户
     *
     * @param key
     * @return
     */
    private static String getInitTenant(String key) {
        return key.substring(key.indexOf("-") + 1, key.indexOf("_"));
    }

    /**
     * 是否写库
     *
     * @param writeOrRead
     * @return
     */
    private static boolean write(WriteOrRead writeOrRead) {
        boolean write = true;
        if (null != writeOrRead && WriteOrRead.READ.name().equals(writeOrRead.name())) {
            write = false;
        }
        return write;
    }

    public static WriteOrRead writeOrRead(ProceedingJoinPoint point, MethodSignature signature) {
        WriteOrRead writeOrRead = WriteOrRead.WRITE;
        //判断方法上的注解
        Write write = signature.getMethod().getAnnotation(Write.class);
        Read read = signature.getMethod().getAnnotation(Read.class);
        if (null != write) {
            return writeOrRead;
        }

        if (null != read) {
            writeOrRead = WriteOrRead.READ;
        } else {
            //判断类上的注解
            write = point.getTarget().getClass().getAnnotation(Write.class);
            read = point.getTarget().getClass().getAnnotation(Read.class);
            if (null != write) {
                return writeOrRead;
            }
            if (null != read) {
                writeOrRead = WriteOrRead.READ;
            }
        }
        return writeOrRead;
    }
}
